﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Data.Common;
using System.Collections;
using System.Reflection;
using System.Globalization;
using System.Collections.ObjectModel;
using LeanEngine.Entity;
using System.Linq.Expressions;
using LeanEngine.Extensions;

namespace LeanEngine.Data.Linq.Mapping
{
    internal sealed class ResultMapper<TEntity> : IEnumerable<TEntity>, IEnumerable
    {
        #region Declaration Members

        private readonly DbDataReader _reader;
        private readonly QueryInfo _info;

        #endregion

        #region Constructor Members

        public ResultMapper(QueryInfo info, DbDataReader reader)
        {
            _reader = reader;
            _info = info;
        }

        #endregion

        #region IEnumerable Members

        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }

        #endregion

        #region IEnumerable<TEntity> Members

        /// <summary>
        /// 1-	If the query expression has projection, compile the projection’s lambda expression to IL
        /// 2-	Find the order of the columns in the result-set returned from the database
        /// 3-	For each row of the result-set:
        ///     a.	Create an instance of the entity
        ///     b.	Populate its members with values from the current row
        ///     c.	Invoke the compiled projection delegate
        /// </summary>
        /// <returns></returns>
        public IEnumerator<TEntity> GetEnumerator()
        {
            BindingFlags bindingFlags = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance;

            var resultList = new List<TEntity>();

            while (_reader.Read())
            {
                var entity = Activator.CreateInstance(_info.SourceMetadata.EntityType, bindingFlags, null, null, null);
                var versionControler = (entity as ChangeTrackerEntity);

                if (_info.CustomBindings == null)
                {
                    DefaultResultMapping((PlainEntity)entity, _reader);
                }
                else
                {
                    int ordinal = 0;
                    foreach (MemberAssignment assignment in this._info.CustomBindings)
                    {
                        BindMemberAssignment(entity, _info.SourceMetadata.EntityType, ref ordinal, assignment, _reader);
                        ordinal++;
                    }
                }

                if (versionControler.IsNotNull()) versionControler.SaveState();

                resultList.Add((TEntity)entity);
            }

            _reader.Close();

            return resultList.GetEnumerator();
        }

        private static void DefaultResultMapping(PlainEntity value, DbDataReader resultSet)
        {
            var indexDictionary = new Dictionary<string, int>(resultSet.FieldCount);
            var metadataTable = MetaTableCache.GetItem(value.GetType());
            foreach (var metadataItem in metadataTable.ColumnsList)
            {
                var columnsNames = metadataItem.DBColumn.Name.Split("|");
                for (int i = 0; i < columnsNames.Length; i++)
                {
                    if (!indexDictionary.ContainsKey(columnsNames[i]))
                    {
                        indexDictionary.Add(columnsNames[i], resultSet.GetOrdinal(metadataTable.TableName + "_" + columnsNames[i]));
                    }

                    if (indexDictionary[columnsNames[i]].IsGreaterThanEqual(0))
                    {
                        ResultMapperExtensions.SetNewValue(metadataItem, columnsNames[i], value, resultSet[indexDictionary[columnsNames[i]]]);
                    }
                }
            }

            var versionControler = (value as ChangeTrackerEntity);
            if (versionControler.IsNotNull()) versionControler.SaveState();
        }

        private void BindMemberAssignment(object entity, Type entityType, ref int ordinal, MemberAssignment assignment, DbDataReader _reader)
        {
            var memberInit = assignment.Expression as MemberInitExpression;
            var property = entityType.GetProperty(assignment.Member.Name);
            if (memberInit != null)
            {
                var subEntity = memberInit.Type.Assembly.CreateInstance(memberInit.Type.FullName);
                for (int i = 0; i < memberInit.Bindings.Count; i++)
                {
                    BindMemberAssignment(subEntity, memberInit.Type, ref ordinal, memberInit.Bindings[i] as MemberAssignment, _reader);
                    if((i + 1) < memberInit.Bindings.Count) ordinal++;
                }

                property.SetValue(entity, subEntity, null);
            }
            else
            {
                var value = _reader[ordinal];
                if (value != null && !(value is DBNull))
                {
                    try
                    {
                        value = Convert.ChangeType(value, property.PropertyType);
                        property.SetValue(entity, value, null);
                    }
                    catch (System.Exception ex)
                    {
                        throw new InvalidCastException("Was not possible to convert " + value.ToString() + " to " + property.DeclaringType.Name + ".", ex);
                    }
                }
            }
        }

        #endregion
    }
}
